package com.atguigu.gulimall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.atguigu.common.tool.utils.Func;
import com.atguigu.common.tool.utils.StringPool;
import com.atguigu.common.utils.PageUtils;
import com.atguigu.common.utils.Query;
import com.atguigu.gulimall.product.dao.CategoryDao;
import com.atguigu.gulimall.product.entity.CategoryBrandRelationEntity;
import com.atguigu.gulimall.product.entity.CategoryEntity;
import com.atguigu.gulimall.product.service.CategoryBrandRelationService;
import com.atguigu.gulimall.product.service.CategoryService;
import com.atguigu.gulimall.product.vo.Catelog2VO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author wenchao.long
 */
@Service("categoryService")
@AllArgsConstructor
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    private final CategoryBrandRelationService categoryBrandRelationService;

    private final StringRedisTemplate redisTemplate;

    private final RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 1. 查出所有分类
        List<CategoryEntity> entities = list();
        // 2. 组装成父子的树形结构
        return entities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .peek(menu -> menu.setChildren(getChildren(menu, entities)))
                .sorted(Comparator.comparing(CategoryEntity::getSort))
                .collect(Collectors.toList());
    }

    @Override
    public void removeMenuByIds(List<Long> ids) {
        // TODO 1. 检查当前删除的菜单 是否被别的地方引用

        // 逻辑删除
        removeByIds(ids);
    }

    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> parentPath = findParentPath(catelogId, new ArrayList<>());
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[0]);
    }

    /**
     * 用@CacheEvict实现失效模式
     * 如果要删除两个缓存 @CacheEvict不能写两个
     * ① 可以用@Caching来替代
     * ② @CacheEvict(value = "category", allEntries = true) 直接删除category分区下所有的数据  (额。。好奇怪)
     * <p>
     * Spring-Cache的不足
     * ①读模式
     * 缓存穿透: 查询一个null数据 解决: 缓存空数据  cache-null-values=true (可以解决)
     * 缓存击穿: 大量并发进来同时查询一个正好过期的数据 解决: 加锁 默认是没有加锁的
     * 缓存雪崩: 大量key同时过期 解决: 加随机过期时间
     * ②写模式(缓存与数据库一致)
     * 读写加锁
     * 引入Canal 感知到MySQL的更新去更新数据库
     * 读多写多 直接去数据库查询就好了
     *
     * @param category category
     */
    @Caching(evict = {
            @CacheEvict(value = "category", key = "'getLevel1Catagories'"),
            @CacheEvict(value = "category", key = "'getCatalogJson'"),
    })
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateDetail(CategoryEntity category) {
        updateById(category);
        String name = category.getName();
        if (Func.isNoneBlank(name)) {
            categoryBrandRelationService.update(
                    Wrappers.<CategoryBrandRelationEntity>lambdaUpdate()
                            .set(CategoryBrandRelationEntity::getCatelogName, name)
                            .eq(CategoryBrandRelationEntity::getCatelogId, category.getCatId())
            );
        }
        // 同时修改缓存中的数据 或者 修改的同时 删除缓存
    }

    /**
     * 1. 代表当前方法的结果需要缓存 如果缓存中有 方法不用调用 如果缓存中没有，会调用方法
     * <p>
     * 2. 每一个需要缓存的数据我们都来指定要放到哪个名字的缓存
     * (按照业务类型)
     * <p>
     * 3. 默认行为
     * ① 如果缓存中有 方法不被调用
     * ② key默认自动生成 缓存的名字：SimpleKey
     * ③ 缓存的value值 默认使用jdk序列化机制 将序列化后的数据存到redis
     * ④ 默认ttl时间 -1
     * 自定义：
     * ① 指定生成的缓存使用的key 用key属性指定，接收一个el表达式
     * ② 指定缓存的数据的存活时间 配置文件中修改ttl
     * ③ 将数据保存为json格式
     *
     * @return List<CategoryEntity>
     */
    @Cacheable(value = {"category"}, key = "#root.method.name")
    @Override
    public List<CategoryEntity> getLevel1Catagories() {
        return list(Wrappers.<CategoryEntity>lambdaQuery()
                .eq(CategoryEntity::getParentCid, StringPool.ZERO));
    }

    @Cacheable(value = {"category"}, key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2VO>> getCatalogJson() {
        List<CategoryEntity> categoryEntities = list();
        // 1. 查出所有1级分类
        List<CategoryEntity> level1Catagories = getParentCid(categoryEntities, 0L);
        // 2. 封装数据
        return level1Catagories.stream()
                .collect(Collectors.toMap(k -> k.getCatId().toString(),
                        v -> {
                            // 1. 每一个1级分类 查到这个一级分类的二级分类
                            List<CategoryEntity> categoryList = getParentCid(categoryEntities, v.getCatId());
                            List<Catelog2VO> voList = Lists.newArrayList();
                            if (Func.isNotEmpty(categoryEntities)) {
                                voList = categoryList.stream()
                                        .map(l2 -> {
                                            // 1. 找当前二级分类的三级分类
                                            List<CategoryEntity> entityList = getParentCid(categoryEntities, l2.getCatId());
                                            List<Catelog2VO.Catelog3VO> vos = Lists.newArrayList();

                                            if (Func.isNotEmpty(entityList)) {
                                                vos = entityList.stream()
                                                        .map(l3 -> Catelog2VO.Catelog3VO.builder()
                                                                .catalog2Id(l2.getCatId().toString())
                                                                .id(l3.getCatId().toString())
                                                                .name(l3.getName())
                                                                .build())
                                                        .collect(Collectors.toList());
                                            }

                                            return Catelog2VO.builder()
                                                    .catalog1Id(v.getCatId().toString())
                                                    .id(l2.getCatId().toString())
                                                    .name(l2.getName())
                                                    .catalog3List(vos)
                                                    .build();
                                        })
                                        .collect(Collectors.toList());
                            }
                            return voList;
                        }
                ));
    }


    /*
    以下内容是结合Redisson Lock处理分布式锁
     */


    //@Override
    //public Map<String, List<Catelog2VO>> getCatalogJson() {
    //    // TODO 产生 堆外内存溢出 OutOfDirectMemoryError
    //    // springBoot2.0 以后默认使用lettuce作为操作Redis的客户端 它使用netty进行网络通信
    //    // lettuce的bug导致netty堆外内存溢出
    //    // lettuce 和 jedis 操作Redis的底层客户端 Spring再次封装redisTemplate
    //
    //    // 1. 加入缓存逻辑
    //    // JSON跨语言 跨平台兼容
    //    ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
    //    String catalogJson = opsForValue.get("catalogJson");
    //    if (Func.isEmpty(catalogJson)) {
    //        // 2. 缓存中没有 查询数据库
    //        System.out.println("缓存不命中...查询数据库...");
    //        return getCatalogJsonFromDbWithRedissonLock();
    //    }
    //    System.out.println("缓存命中");
    //    return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2VO>>>() {
    //    });
    //}

    /**
     * 缓存里面的数据 如何与数据库保持一致
     * 缓存数据一致性
     * 1)、 双写模式 更新数据库同时更新缓存
     * 2)、 失效模式 更新数据库同时删除缓存
     */
    private Map<String, List<Catelog2VO>> getCatalogJsonFromDbWithRedissonLock() {

        // 1. 占锁 去redis占坑
        // 锁的粒度 越细越快 具体缓存的是某个数据     product-11-lock
        RLock lock = redissonClient.getLock("catalogJson-lock");

        lock.lock();

        Map<String, List<Catelog2VO>> dataFromDB;
        try {
            dataFromDB = getDataFromDB();
        } finally {
            lock.unlock();
        }
        return dataFromDB;
    }

    private Map<String, List<Catelog2VO>> getCatalogJsonFromDbWithRedisLock() {

        ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
        String uuid = Func.randomUUID();
        // 1. 占分布式锁 去redis占坑
        Boolean lock = opsForValue.setIfAbsent("lock", uuid, Duration.ofSeconds(30));
        if (Boolean.TRUE.equals(lock)) {
            System.out.println("获取分布式锁成功....");
            //加锁成功... 执行业务
            // 2. 设置过期时间 必须和加锁是同步的
            //redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
            Map<String, List<Catelog2VO>> dataFromDB;
            try {
                dataFromDB = getDataFromDB();
            } finally {
                // 删除锁
                //redisTemplate.delete("lock");

                // 获取值对比+对比成功删除=原子操作
                //String lockValue = opsForValue.get("lock");
                //if (uuid.equals(lockValue)) {
                //    // 删除我自己的锁
                //    redisTemplate.delete("lock");
                //}

                // 使用lua脚本
                String string = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                redisTemplate.execute(new DefaultRedisScript<>(string, Long.class), Lists.newArrayList("lock"), uuid);
            }
            return dataFromDB;
        } else {
            // 加锁失败...重试
            // 休眠100ms重试
            // 自旋的方式
            try {
                TimeUnit.SECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("获取分布式锁失败...等待重试");
            return getCatalogJsonFromDbWithRedisLock();
        }
    }

    private Map<String, List<Catelog2VO>> getDataFromDB() {
        ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
        String catalogJson = opsForValue.get("catalogJson");
        if (Func.isNotEmpty(catalogJson)) {
            // 缓存不为null 直接返回
            return JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2VO>>>() {
            });
        }

        System.out.println("查询了数据库..." + Thread.currentThread().getName());

        List<CategoryEntity> categoryEntities = list();
        // 1. 查出所有1级分类
        List<CategoryEntity> level1Catagories = getParentCid(categoryEntities, 0L);
        // 2. 封装数据
        Map<String, List<Catelog2VO>> map = level1Catagories.stream()
                .collect(Collectors.toMap(k -> k.getCatId().toString(),
                        v -> {
                            // 1. 每一个1级分类 查到这个一级分类的二级分类
                            List<CategoryEntity> categoryList = getParentCid(categoryEntities, v.getCatId());
                            List<Catelog2VO> voList = Lists.newArrayList();
                            if (Func.isNotEmpty(categoryEntities)) {
                                voList = categoryList.stream()
                                        .map(l2 -> {
                                            // 1. 找当前二级分类的三级分类
                                            List<CategoryEntity> entityList = getParentCid(categoryEntities, l2.getCatId());
                                            List<Catelog2VO.Catelog3VO> vos = Lists.newArrayList();

                                            if (Func.isNotEmpty(entityList)) {
                                                vos = entityList.stream()
                                                        .map(l3 -> Catelog2VO.Catelog3VO.builder()
                                                                .catalog2Id(l2.getCatId().toString())
                                                                .id(l3.getCatId().toString())
                                                                .name(l3.getName())
                                                                .build())
                                                        .collect(Collectors.toList());
                                            }

                                            return Catelog2VO.builder()
                                                    .catalog1Id(v.getCatId().toString())
                                                    .id(l2.getCatId().toString())
                                                    .name(l2.getName())
                                                    .catalog3List(vos)
                                                    .build();
                                        })
                                        .collect(Collectors.toList());
                            }
                            return voList;
                        }
                ));

        // 3. 查到的数据在放入缓存
        String string = JSON.toJSONString(map);
        opsForValue.set("catalogJson", string, Duration.ofSeconds(120));
        return map;
    }

    private Map<String, List<Catelog2VO>> getCatalogJsonFromDbWithLocalLock() {

        /*
         * 1. 空结果缓存：解决缓存穿透
         * 2. 设置过期时间(随机)：解决缓存雪崩
         * 3. 加锁：解决缓存击穿
         */

        // 只要是同一把锁 就能锁住需要这个锁的所有线程
        // 1.synchronized (this): SpringBoot所有组件在容器 中都是单例的 所以可行
        // TODO 本地锁 synchronized或者JUC(Lock) 都是单体版 在分布式情况下 想要锁住所有 要分布式锁

        synchronized (this) {
            // 得到锁以后  我们应该再去缓存中确定一次 如果没有才继续查询
            return getDataFromDB();
        }
    }

    private List<CategoryEntity> getParentCid(List<CategoryEntity> categoryEntities, Long parentCid) {
        return categoryEntities.stream()
                .filter(c -> c.getParentCid().equals(parentCid))
                .collect(Collectors.toList());
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        paths.add(catelogId);
        CategoryEntity entity = getById(catelogId);
        if (entity != null && entity.getParentCid() != 0) {
            findParentPath(entity.getParentCid(), paths);
        }
        return paths;
    }

    /**
     * 递归查找所有菜单的子菜单
     *
     * @param root 当前菜单
     * @param all  所有菜单
     * @return 子菜单
     */
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        return all.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId()))
                // 找子菜单
                .peek(categoryEntity -> categoryEntity.setChildren(getChildren(categoryEntity, all)))
                // 菜单的排序
                .sorted(Comparator.comparing(CategoryEntity::getSort))
                .collect(Collectors.toList());
    }
}
